#!/usr/bin/env python

#Copyright (C) 2008 Luis Felipe Millani (lf.millani@gmail.com)

#This file is part of MagicLyrics.

#MagicLyrics is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.

#MagicLyrics is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.

#You should have received a copy of the GNU General Public License
#along with MagicLyrics.  If not, see <http://www.gnu.org/licenses/>.
"""
	to-do:
		fix create_exe to include COPYING and LICENSE
		there must exist a library function that does what get_text_from_page does
		clean up get_lyrics() a bit because it's became almost completely illegible
		fix threads, busy wait is bad
		clean gui
		maybe, just for the comparisons, remove special chars('"','\'',etc) from the lyrics
		give preference to the page that has more special chars? (this way we get "it's" instead of "its")
		remove_bad_stuff_from_song seems to be doing more bad than good
		if lyrics=sequency_of(&raquo;) then lyrics=''
		find a better choice of terms for the query
			alternative solution: do more than a single query when needed
		only use guess_encoding if the page doesn't says the encoding already
		sometimes &quote and &eraquo; appear in the lyrics (do _not_ fix this on main, do it on module get_text_from_page)
		discard lyrics in which every line's less than n chars
		lyricsFile should cause a exception on failure
		add update button
		identify when a page displays part of the lyrics on a single line while another page displays the same info on two lines
		allow manual edition of the lyrics, and save it
		clean up the code
		detect which song is playing on:
			win32:
				foobar
				winamp (sigh)
				itunes
			*nix
				xmms
				basilisk
				amaroK
		clean up the code
		?????
		PROFIT!!
"""

import sys
import os

# modules
from netsearch import netsearch
from download import download
from guess_encoding import guess_encoding
import threads
from get_text_from_page import get_text_from_page
from lcs import lcs

LYRICS_DIR='lyrics'





def get_lyrics(song,max_sources=5,min_lines=7,max_dist=5,status_report=lambda x:None,force=False,do_after=lambda x:None):
	"""
	get_lyrics(..) -> unicode
	
	find lyrics for given song (returns u'' if lyrics couldn't be found)
	
	song        song for which we want to find the lyrics.
	            should be in the format (artist,song)
	            e.g. ("Ozma","Gameover")
	max_sources maximum number of pages to fetch (should be n | 1<=n<=10)
	min_lines   minimum number of lines the lyrics should have
	max_dist    (maximum distance between two different lines of the lyrics on the page)
				noise tolerance; higher values raise the probability of
	            finding the lyrics but also raise the noise in the
	            resulting lyrics
	status_report	call this function with the current status (searching/processing) as parameter
	force       if True lyrics won't be loaded from disk
	do_after	call this function passing found lyrics as parameter after the lyrics have been found
	"""
	def remove_bad_stuff_from_song(song):
		"""
		remove_bad_stuff_from_song(...) -> tuple
		
		remove undesired chars from the (song/artist)'s name
		"""
		result=[]
		for item in (song[0],song[1]):
			for a,b in ('(',')'),('[',']'),('{','}'),('<','>'):
				left=item.find(a)
				right=item.find(b)
				while left!=-1 or right!=-1:
					if -1 not in (left,right):
						if left<right:item=item[:left]+' '+item[right+1:]
						else:item=item.replace(b,'',1)
					else:
						item=item.replace(a,'',1).replace(b,'',1)
					left=item.find(a)
					right=item.find(b)
			for a in (' -','+','=',':'):item=item.replace(a,'')
			while item.find('  ')>-1:item=item.replace('  ',' ')
			result.append(item.strip(' ').strip('-').strip(' '))
		return (result[0],result[1])
	def lyricsFile(song):
		"""
		lyricsFile(song) -> string
		
		get lyrics filename, if valid
		"""
		song=(song[0]+'.-.'+song[1]+'.txt').replace(' ','.').lstrip('.')
		song=reduce(lambda a,b:a.replace(chr(b),''),[song]+range(0,32)
			+[ord('<'),ord('>'),ord(':'),ord('"'),ord('/'),ord('\\')
			,ord('|'),ord('?'),ord('*')])
		return os.path.join(LYRICS_DIR,song)
	def lyricsLoad(song):
		"""
		lyricsLoad(song) -> string or None
		
		search for lyrics file, if found, return it's text,
		otherwise return None
		"""
		file=lyricsFile(song)
		if os.path.isfile(file):
			f=open(file,'rb')
			lyrics=f.read()
			f.close()
			try:
				return guess_encoding(lyrics)[0]
			except:
				print >> sys.stderr,'failed to decode text in file "%s"\n'%file
		return None
	def lyricsSave(song,lyrics,encoding='utf8'):
		"""
		save lyrics to disk
		"""
		if not os.path.isdir(LYRICS_DIR):
			os.mkdir(LYRICS_DIR)
		file=lyricsFile(song)
		try:
			f=open(file,'wt')
		except IOError:
			print >> sys.stderr,'failed to open file "%s" for writting\n'%file
		else:
			f.write(lyrics.encode(encoding))
			f.close()
	def get_lyrics_pages(search,max):
		"""
		get_lyrics_pages(...) -> []
		
		search for 'search' and return the first 'max' pages
		"""
		google=netsearch(search,max=max*4,engine='google',fetch=download)
		
		src=[]
		while max>len(src) and len(google)>0:
			urls=[]
			left=max-len(src)
			while len(urls)<left and len(google)>0:
				urls.append(google.pop(0))
			urls=threads.batch(download,urls,max_threads=0)
			for url in urls:src.append(url)
			while None in src:src.remove(None)
		return src
	
	def largest_group(nums,dist):
		"""
		largest_group(..) -> list
		
		find the largest group of numbers in "nums" in which the maximum
		difference between any two numbers is <="dist"
		
		nums    elements for x
		dist    maximum distance
		"""
		nums.sort() 
		largest=[]
		for i in range(0,len(nums)-1):
			current=[nums[i]]
			for j in range(i,len(nums)-1):
				if 0<=(nums[j]-nums[j+1])<=dist or 0<=(nums[j+1]-nums[j])<=dist:
					current.append(nums[j+1])
				else:
					break
			if len(current)>len(largest):
				largest=current
		return largest
	
	if song[1]=='':
		status_report('')
		do_after(u'')
		return u''
	if not force:
		search=lyricsLoad(song)
		if search!=None:
			status_report('')
			do_after(search)
			return search
	search=remove_bad_stuff_from_song(song)
	#search='intitle:lyrics intitle:"'+search[0]+'" intitle:"'+search[1]+'"'
	#search='intitle:lyrics "'+search[0]+'" "'+search[1]+'"'
	#search='intitle:lyrics "'+search[0]+'" intitle:"'+search[1]+'" | inurl:"'+search[1]+'"'
	#search='intitle:lyrics '+search[0]+' '+search[1]
	#search='intitle:lyrics "'+search[0]+'" intitle:"'+search[1]+'" | inurl:"'+search[1]+'" | "'+search[1]+'"'
	#search='lyrics "'+search[0]+'" intitle:"'+search[1]+'" | inurl:"'+search[1]+'"'
	search='intitle:lyrics "'+search[0]+'" intitle:"'+search[1]+'" | inurl:"'+search[1]+'"'
	status_report('searching...')
	try:
		#pages,encodings=get_lyrics_pages(search,max_sources)
		pages=get_lyrics_pages(search,max_sources)
	except:
		#print >> sys.stderr,'failed to download pages for query (%s)\n'%search
		status_report('')
		do_after(u'')
		return u''
	if pages==[]:
		#print 'no results found for query (%s)\n'%search
		status_report('')
		do_after(u'')
		return u''
	status_report('processing data...')
	pages=map(lambda x:map(lambda x:x.strip(' .\t'),get_text_from_page(x).splitlines()),pages) #divide the pages texts in lines, stripping ' .\t'
	pages=filter(lambda x:x>=min_lines,pages)
	#i=0
	#while i<len(pages):
	#	if len(pages[i])<min_lines:
	#		pages.pop(i)
	#		#encodings.pop(i)
	#	else:
	#		i+=1
	
	#hashes=map(lambda pg:map(lambda ln:hash(ln.lower()),pg),pages) # calculate the hash for each line of every page
	hashes=[]
	for pg in pages:
		pg_hashes=[]
		for i in xrange(len(pg)-1,-1,-1):
			ln_clean=filter(lambda c:c.isalnum(),pg[i].lower())
			if ln_clean=='':
				pg.pop(i)
			else:
				pg_hashes.append(hash(ln_clean))
		pg_hashes.reverse()
		hashes.append(pg_hashes)
	del pg_hashes,ln_clean
	
	
	
	
	# remove some noise from the pages through multiple comparisons
	
	# find noise
	candidates=[]
	for i in xrange(0,len(pages)-1):
		left=len(pages[i])-1	# up to page[:left] we'll have only noise
								# len(pages[i])-1: rightmost position
		right=0	# from page[right:] onward there'll be nothing but noise
				# 0: leftmost position
		for j in xrange(i+1,len(pages)):
			curr_lcs=largest_group(lcs(hashes[i],hashes[j]),max_dist) # lyrics lines aren't too distant from each other, so we can remove lines which are too far
			if len(curr_lcs)>=min_lines:
				if curr_lcs[0]-max_dist<left:
					if curr_lcs[0]-max_dist<=0:
						left=0
					else:
						left=curr_lcs[0]-max_dist # there's a tolerance margin - max_dist - because some of what we think is noise might not be it
				if curr_lcs[len(curr_lcs)-1]+max_dist>right:
					if curr_lcs[len(curr_lcs)-1]+max_dist>=len(pages[i]):
						right=len(pages[i])-1
					elif curr_lcs[len(curr_lcs)-1]>right:
						right=curr_lcs[len(curr_lcs)-1] # there's a tolerance margin - max_dist - because some of what we think is noise might not be it
		if left>right:
			left,right=right,left
		candidates.append([left,right])
	
	# remove noise
	for i in xrange(len(candidates)):
		for j in range(candidates[i][1]+1,len(pages[i])):
			pages[i].pop()
			hashes[i].pop()
		for j in range(0,candidates[i][0]):
			pages[i].pop(0)
			hashes[i].pop(0)
	
	
	
	# the lyrics are what the pages have in common, now that we removed some of the noise
	lyrics=[]
	artist,song_name=map(lambda s:reduce(lambda x,y:x.replace(y,''),(s.lower(),' ',',','.','!','?','-','\'','"',':')),(song[0],song[1]))
	for i in range(0,len(pages)-1):
		for j in range(i+1,len(pages)):
			candidate=lcs(hashes[i],hashes[j])
			candidate=map(lambda ln:pages[i][ln],candidate)
			
			try:
				encoding=guess_encoding('\n'.join(candidate))[1]
			except UnicodeError:continue
			except ValueError:continue
			except StandardError:continue
			
			candidate=map(lambda ln:unicode(ln,encoding),candidate)
			# remove noise from first few lines
			for k in xrange(min(10,len(candidate)-1),-1,-1):
				lower=reduce(lambda x,y:x.replace(y,''),(candidate[k].lower(),' ',',','.','!','?','-','\'','"',':'))
				if not u''==lower and u''==lower.replace(song_name,''):continue
				if u''==reduce(lambda x,y:x.replace(y,''),(lower,' ',',','.','!','?','-','\'','"',':',artist,song_name,'search','lyrics','artist','title','soundtrack','album','song')):
					candidate.pop(k)
			if len(candidate)>len(lyrics):
				lyrics=candidate
	del artist,song_name
	if len(lyrics)<min_lines:
		s=(u'','utf8')
		lyricsSave(song,s[0])
	else:
		s=(unicode('\n',encoding).join(lyrics),encoding)
		lyricsSave(song,s[0],s[1])
	status_report('')
	do_after(s[0])
	return s[0]




def nogui(player):
	"""
	secondary interface
	you can't manually change the song in this
	"""
	playing=('','')
	import time
	while 1:
		if player.playing() not in (playing,('',''),None):
			playing=player.playing()
			print '\n\n\n'+playing[0]+' - '+playing[1]+'\n'
			print get_lyrics(playing)
		time.sleep(1)
#	




def main(argv=[]):
	import sys
	player=__import__('player').Player()
	if len(sys.argv)==1:
		__import__('gui').start(player.playing,get_lyrics)
		#try:__import__('gui').start(player.playing,get_lyrics)
		#except:
		#	print 'ERROR: failed to load gui module, will work in cli mode\n       maybe you don\'t have wxPython installed?'
		#	nogui(player)
	elif len(sys.argv)==2 and sys.argv[1]=='-cli':
		nogui(player)
	else:
		print 'usage: '+sys.argv[0]+' [-cli]\n   cli - start in command-line mode'
if __name__=='__main__':
	main()
